);
}
// ─── A single draggable / removable "My subject" chip ─────────────────────
// Click to select. Press-and-drag to reorder within the Subject area, or drag
// it out of the dashed drop zone to delete (like dragging a file off a field).
// The × button is an always-reachable explicit remove.
function MySubjectChip({ name, label, active, disabled, ghost, chipRef, onPointerDown, onSelect, onRemove }) {
return (
);
}
// ─── Subject chips (country-curriculum-first) ─────────────────────────────
function SubjectChips({ subject, onPick, onChangeMySubjects, country, gradeLevel, mySubjects, disabled, feedbackLanguage }) {
const t = useT();
const [expanded, setExpanded] = useState(false);
const curriculum = country ? subjectsForLevel(country, gradeLevel, feedbackLanguage || "English") : null;
const rawList = curriculum || SUBJECTS;
// Normalize so "Physics", "physics" and " Physics " are treated as one.
const norm = (s) => (s || "").trim().toLowerCase();
// The Subject section is a single editable, draggable working set. It lives
// in prefs.mySubjects once the user has customised it. Until then we SEED it
// from the country curriculum so the chips on the main page are ALWAYS
// reorderable / removable — and we persist that seed the moment the user
// makes their first edit (see baseForMutation), "promoting" it to their own
// list. This is why an empty mySubjects no longer means "nothing to drag".
const SEED_COUNT = 8;
const hasCustom = !!(mySubjects && mySubjects.length > 0);
const effectiveMine = hasCustom
? mySubjects
: rawList.slice(0, SEED_COUNT).map((n) => ({ name: n, level: null }));
const myNorms = new Set(effectiveMine.map((s) => norm(mySubjectLabel(s))));
// Suggestions = curriculum subjects not already in the working set.
const baseList = rawList.filter((s) => !myNorms.has(norm(s)));
// Collapsed, show only the first few subjects; "view more" reveals the rest
// of the working set AND the country's other subjects.
const MINE_VISIBLE = 3;
const shownMine = expanded ? effectiveMine : effectiveMine.slice(0, MINE_VISIBLE);
const hiddenCount = (effectiveMine.length - shownMine.length) + baseList.length;
// The active subject is already represented if it's in the curriculum row or
// the working-set row — only then do we skip the standalone "active" chip.
const inList = rawList.some((s) => norm(s) === norm(subject));
const inMine = myNorms.has(norm(subject));
// Basis for any mutation: the real persisted list if customised, otherwise
// the seed (which then becomes persisted on save).
const baseForMutation = () => (hasCustom ? mySubjects : effectiveMine);
// Add a subject to the working set (and select it).
const addMySubject = (name) => {
const v = (name || "").trim();
if (!v) return;
onPick(v);
if (!onChangeMySubjects) return;
const base = baseForMutation();
if (!base.some((s) => norm(s.name) === norm(v))) onChangeMySubjects([...base, { name: v, level: null }]);
};
// Remove a subject from the working set (works on desktop + mobile since both
// read from the same prefs.mySubjects).
const removeMySubject = (name) => {
if (!onChangeMySubjects) return;
onChangeMySubjects(baseForMutation().filter((s) => norm(s.name) !== norm(name)));
};
// ── Free-drag controller: reorder inside the zone, drop outside to delete ──
// Uses window-level pointer listeners (NOT setPointerCapture): iOS Safari has
// a long-standing bug where capturing a touch-derived pointer stops
// pointermove from firing. Window listeners + touch-action:none on the chip
// give reliable dragging across desktop and mobile (incl. iPhone).
const mineRef = useRef(null);
const chipEls = useRef({}); // name -> DOM node
const sess = useRef(null); // active pointer session
const suppressClick = useRef(false);
const [drag, setDrag] = useState(null); // { name, idx, x, y, dxOff, dyOff, w, h, outside, overIdx }
const dragRef = useRef(null);
dragRef.current = drag;
// Mutable mirrors so the once-installed window listeners never read stale
// data. mySubjectsRef points at the EFFECTIVE list (seed or real) so a drag
// started before any edit still reorders/removes against the right array.
const mySubjectsRef = useRef(effectiveMine); mySubjectsRef.current = effectiveMine;
const onChangeRef = useRef(onChangeMySubjects); onChangeRef.current = onChangeMySubjects;
const insideZone = (x, y) => {
const el = mineRef.current;
if (!el) return true;
const r = el.getBoundingClientRect();
const m = 28; // tolerance so chips near the edge still count as "inside"
return x >= r.left - m && x <= r.right + m && y >= r.top - m && y <= r.bottom + m;
};
const computeOverIdx = (x, y) => {
const list = mySubjectsRef.current || [];
let over = list.length;
for (let i = 0; i < list.length; i++) {
const el = chipEls.current[list[i].name];
if (!el) continue;
const r = el.getBoundingClientRect();
const after = y > r.bottom || (y >= r.top && x > r.left + r.width / 2);
if (!after) { over = i; break; }
}
return over;
};
const doRemove = (name) => {
const fn = onChangeRef.current; if (!fn) return;
fn((mySubjectsRef.current || []).filter((s) => norm(s.name) !== norm(name)));
};
const doReorder = (from, over) => {
const fn = onChangeRef.current; if (!fn) return;
const arr = [...(mySubjectsRef.current || [])];
if (from < 0 || from >= arr.length) return;
const [item] = arr.splice(from, 1);
let to = over; if (from < over) to -= 1;
to = Math.max(0, Math.min(arr.length, to));
arr.splice(to, 0, item);
fn(arr);
};
// Install global move/up listeners ONCE. They early-return unless a drag
// session is live, so they're cheap when idle.
useEffect(() => {
const onMove = (e) => {
const s = sess.current;
if (!s) return;
if (!s.moved) {
const slop = s.pointerType === "touch" ? 8 : 4;
if (Math.abs(e.clientX - s.startX) < slop && Math.abs(e.clientY - s.startY) < slop) return;
s.moved = true;
}
if (e.cancelable) e.preventDefault();
const inside = insideZone(e.clientX, e.clientY);
setDrag({ name: s.name, idx: s.idx, x: e.clientX, y: e.clientY, dxOff: s.dxOff, dyOff: s.dyOff, w: s.w, h: s.h, outside: !inside, overIdx: inside ? computeOverIdx(e.clientX, e.clientY) : null });
};
const onUp = () => {
const s = sess.current;
sess.current = null;
const d = dragRef.current;
if (s && s.moved && d) {
suppressClick.current = true;
setTimeout(() => { suppressClick.current = false; }, 60);
if (d.outside) doRemove(s.name);
else doReorder(s.idx, d.overIdx == null ? (mySubjectsRef.current || []).length : d.overIdx);
}
setDrag(null);
};
window.addEventListener("pointermove", onMove, { passive: false });
window.addEventListener("pointerup", onUp);
window.addEventListener("pointercancel", onUp);
return () => {
window.removeEventListener("pointermove", onMove, { passive: false });
window.removeEventListener("pointerup", onUp);
window.removeEventListener("pointercancel", onUp);
};
}, []);
const onChipPointerDown = (e, name) => {
if (disabled) return;
if (e.pointerType === "mouse" && e.button !== 0) return;
const list = mySubjectsRef.current || [];
const idx = list.findIndex((s) => s.name === name);
const el = chipEls.current[name];
if (!el) return;
const r = el.getBoundingClientRect();
sess.current = { name, idx, pointerType: e.pointerType, startX: e.clientX, startY: e.clientY, dxOff: e.clientX - r.left, dyOff: e.clientY - r.top, w: r.width, h: r.height, moved: false };
};
const selectChip = (label) => { if (!suppressClick.current && !disabled) onPick(label); };
const dragLabel = drag ? mySubjectLabel((effectiveMine || []).find((s) => s.name === drag.name) || { name: drag.name }) : "";
return (
);
}
// ─── Logo ────────────────────────────────────────────────────────────────
// Real GradeCheck mark embedded as data — no external file, works on every route.
const GC_LOGO_SRC = "data:image/png;base64,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";
const GC_WORDMARK_SRC = "data:image/png;base64,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";
function Logo({ size = 28, color = "#63D378" }) {
// Exact icon pixels cropped from the brand art (no redraw), cut to the
// document+check silhouette so it sits transparently on any surface (181×226)
const w = size * 0.92;
const h = w / (181 / 226);
return (
);
}
function WordMark({ size = 18, name = "GradeCheck", accent = "Check", color = "#63D378" }) {
// split the name into [pre, accent, post] and color the accent portion
const i = accent ? name.indexOf(accent) : -1;
const textEl = i < 0
? {name}
:
{name.slice(0, i)}
{name.slice(i, i + accent.length)}
{name.slice(i + accent.length)}
;
// Default brand: exact wordmark pixels from the brand art on dark surfaces
// (art is white+green on black, so light theme keeps the live text version).
// Theme is tracked in JS (not just CSS) so only one of the two ever mounts —
// no reliance on a stylesheet load/cache to hide the other and cause ghosting/duplication.
const [dark, setDark] = useState(() => typeof document !== "undefined" && document.documentElement.dataset.theme === "dark");
useEffect(() => {
const root = document.documentElement;
const sync = () => setDark(root.dataset.theme === "dark");
sync();
const obs = new MutationObserver(sync);
obs.observe(root, { attributes: true, attributeFilter: ["data-theme"] });
return () => obs.disconnect();
}, []);
if (name !== "GradeCheck" || accent !== "Check") return textEl;
if (!dark) return textEl;
return (
);
}
// ─── Auth Modal (gated, not full screen) ─────────────────────────────────
function AuthModal({ reason, onAuth, onClose, tweaks }) {
const t = useT();
const [mode, setMode] = useState("signup");
// step: "credentials" | "code" | "forgot" | "forgot-code"
const [step, setStep] = useState("credentials");
const [name, setName] = useState("");
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const [code, setCode] = useState("");
const [newPassword, setNewPassword] = useState("");
const [authErr, setAuthErr] = useState("");
const [authBusy, setAuthBusy] = useState(false);
const [googleBusy, setGoogleBusy] = useState(false);
const handleGoogleAuth = async () => {
if (!supabaseClient) return;
setGoogleBusy(true);
setAuthErr("");
try {
// Get the Google OAuth URL without navigating — we'll open it in a popup
// so the main page stays fully loaded (no white screen, no reload).
const { data, error } = await supabaseClient.auth.signInWithOAuth({
provider: "google",
options: {
redirectTo: window.location.origin + window.location.pathname,
skipBrowserRedirect: true,
},
});
if (error) throw error;
if (!data?.url) throw new Error("Could not start Google sign-in");
// Open the Google account picker in a small centred popup.
const pw = 520, ph = 640;
const pl = Math.max(0, Math.round((screen.width - pw) / 2));
const pt = Math.max(0, Math.round((screen.height - ph) / 4));
const popup = window.open(
data.url,
"gradecheck-google-auth",
`width=${pw},height=${ph},left=${pl},top=${pt},scrollbars=yes,resizable=yes`
);
if (!popup) {
// Popup blocked — fall back to standard full-page redirect.
window.location.href = data.url;
return;
}
let completed = false;
// OAuthPopupScreen (rendered inside the popup after Google redirects back)
// exchanges the PKCE code for a session, then posts this message.
const onMsg = (e) => {
if (e.origin !== window.location.origin) return;
if (e.data?.type !== "GRADECHECK_OAUTH_DONE") return;
completed = true;
window.removeEventListener("message", onMsg);
clearInterval(closePoller);
clearTimeout(safetyTimer);
try { popup.close(); } catch (_) {}
const session = e.data.session;
const sessEmail = (session?.user?.email || "").toLowerCase().trim();
const googleName =
session?.user?.user_metadata?.full_name ||
session?.user?.user_metadata?.name ||
sessEmail.split("@")[0] || "";
const saved = loadUser();
const sessName = (saved && saved.email === sessEmail && saved.name) ? saved.name : googleName;
onAuth({ email: sessEmail, name: sessName, plan: "free" }, false);
setGoogleBusy(false);
};
window.addEventListener("message", onMsg);
// Detect the user closing the popup without finishing.
const closePoller = setInterval(() => {
try {
if (popup.closed && !completed) {
clearInterval(closePoller);
clearTimeout(safetyTimer);
window.removeEventListener("message", onMsg);
setAuthErr("Sign-in was cancelled. Try again.");
setGoogleBusy(false);
}
} catch (_) {}
}, 400);
// 2-minute safety timeout.
const safetyTimer = setTimeout(() => {
clearInterval(closePoller);
window.removeEventListener("message", onMsg);
if (!completed) {
try { popup.close(); } catch (_) {}
setGoogleBusy(false);
}
}, 120_000);
} catch (e) {
setAuthErr(e.message || "Google sign-in failed. Try again.");
setGoogleBusy(false);
}
};
const finalizeAuth = (user) => {
saveUser(user);
if (mode === "signup") {
try {
const k = "mr:daily";
const d = JSON.parse(localStorage.getItem(k) || "{}");
if (d && typeof d.count === "number" && d.count > 0) {
d.count = Math.max(0, d.count - 1);
localStorage.setItem(k, JSON.stringify(d));
}
} catch {}
fetch("/api/welcome", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ email: user.email, name: user.name })
}).catch(() => {});
}
onAuth(user, mode === "signup");
};
const submitCredentials = async (e) => {
e.preventDefault();
if (!email.trim() || !password) return;
setAuthErr("");
setAuthBusy(true);
try {
const endpoint = mode === "signup" ? "/api/signup" : "/api/login";
const r = await fetch(endpoint, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ email: email.trim(), password, name: name.trim() }),
});
const data = await r.json().catch(() => ({}));
setAuthBusy(false);
if (!r.ok) {
setAuthErr(data.error || "Auth failed");
return;
}
if (data.needsCode) {
setStep("code");
setCode("");
return;
}
if (data.user) finalizeAuth(data.user);
} catch {
setAuthBusy(false);
setAuthErr("Could not reach server. Check your connection and try again.");
}
};
const submitCode = async (e) => {
e.preventDefault();
if (!code.trim()) return;
setAuthErr("");
setAuthBusy(true);
try {
const r = await fetch("/api/verify", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ email: email.trim().toLowerCase(), code: code.trim() }),
});
const data = await r.json().catch(() => ({}));
setAuthBusy(false);
if (!r.ok) {
setAuthErr(data.error || "Verification failed");
return;
}
if (data.user) finalizeAuth(data.user);
} catch {
setAuthBusy(false);
setAuthErr("Could not reach server. Try again.");
}
};
const switchMode = (m) => {
setMode(m);
setStep("credentials");
setAuthErr("");
setCode("");
};
const requestReset = async () => {
if (!email.trim()) { setAuthErr("Enter your email first"); return; }
setAuthErr("");
setAuthBusy(true);
try {
const r = await fetch("/api/forgot", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ email: email.trim() }),
});
const data = await r.json().catch(() => ({}));
setAuthBusy(false);
if (!r.ok) { setAuthErr(data.error || "Could not send code"); return; }
setStep("forgot-code");
setCode("");
} catch {
setAuthBusy(false);
setAuthErr("Could not reach server. Try again.");
}
};
const submitReset = async (e) => {
e.preventDefault();
if (!code.trim() || !newPassword) return;
setAuthErr("");
setAuthBusy(true);
try {
const r = await fetch("/api/reset", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ email: email.trim().toLowerCase(), code: code.trim(), password: newPassword }),
});
const data = await r.json().catch(() => ({}));
setAuthBusy(false);
if (!r.ok) { setAuthErr(data.error || "Reset failed"); return; }
if (data.user) finalizeAuth(data.user);
} catch {
setAuthBusy(false);
setAuthErr("Could not reach server. Try again.");
}
};
return (
{
step === "code" ? `We sent a 6-digit code to ${email}. Enter it below.` :
step === "forgot" ? t("auth_forgot_detail") :
step === "forgot-code" ? (t("auth_check_email") + " — a code is on its way to " + email + ".") :
(mode === "signup" ? t("auth_signup_detail") : (reason?.detail || t("auth_login_detail")))
}
{eff.icon} {eff.label} · {mcqScore.timeUsed}/{mcqScore.timeLimitMin} min
);
})()}
{mcqScore.correct===mcqScore.total ? "Perfect score!" :
mcqScore.correct>=mcqScore.total*0.8 ? "Strong result" :
mcqScore.correct>=mcqScore.total*0.6 ? "Decent — a few to review" :
"Review the topic and try again"}
);
}
// ─── Grade view ──────────────────────────────────────────────────────────
const RUBRIC_TEMPLATES = [
{ label: "5-para essay",
text: "Grade this five-paragraph essay on: strength of the thesis and introduction, quality of body paragraph arguments and evidence, whether a counterargument is addressed, effectiveness of the conclusion, and writing mechanics and style." },
{ label: "Lab report",
text: "Grade this lab report on: clarity of aim and hypothesis, how well the method is described, accuracy and presentation of results, quality of scientific analysis and reasoning, and strength of the conclusion and evaluation." },
{ label: "Timed essay",
text: "Grade this timed essay on: how directly it answers the question, how well the argument is structured, quality and relevance of evidence and examples, and clarity and fluency of expression." },
{ label: "Discursive",
text: "Grade this discursive essay on: whether it presents a balanced argument with both sides, quality and credibility of evidence used, how well it's structured and coherent, appropriate formal language register, and effectiveness of the conclusion." },
{ label: "Coding project",
text: "Grade this coding project on: whether the code produces correct output, how well-structured and readable the code is, how edge cases and errors are handled, quality of comments and variable naming, and overall efficiency of the solution." },
{ label: "Presentation",
text: "Grade this presentation on: accuracy and depth of content, how logically it's structured and flows between points, clarity of explanation and how well it engages the audience, and how well claims are supported by evidence or examples." },
];
// ─── Sparkline ───────────────────────────────────────────────────────────
function Sparkline({ scores, color = "currentColor", width = 88, height = 32 }) {
if (!scores || scores.length < 2) return null;
const W = width, H = height, PAD = 3;
const min = Math.min(...scores), max = Math.max(...scores);
const range = max - min || 1;
const n = scores.length;
const pts = scores.map((s, i) => ({
x: PAD + (i / (n - 1)) * (W - PAD * 2),
y: (H - PAD - 4) - ((s - min) / range) * (H - PAD * 2 - 4),
}));
// Smooth bezier path
const d = pts.reduce((acc, p, i) => {
if (i === 0) return `M ${p.x} ${p.y}`;
const prev = pts[i - 1];
const cpx = (prev.x + p.x) / 2;
return acc + ` C ${cpx} ${prev.y}, ${cpx} ${p.y}, ${p.x} ${p.y}`;
}, "");
// Area fill (closed path back along bottom)
const area = d + ` L ${pts[pts.length-1].x} ${H} L ${pts[0].x} ${H} Z`;
const last = pts[pts.length - 1];
const gradId = `spark-${color.replace(/[^a-z0-9]/gi,'')}`;
const clipId = `clip-${gradId}`;
return (
);
}
// ─── Next-grade nudge helper ─────────────────────────────────────────────
function getNextGradeInfo(score, system) {
if (!system || !Array.isArray(system.bands)) return null;
const bands = system.bands; // sorted highest-first
let currentIdx = -1;
for (let i = 0; i < bands.length; i++) {
if (score >= bands[i].min) { currentIdx = i; break; }
}
if (currentIdx <= 0) return null; // already at top, or not found
const nextBand = bands[currentIdx - 1];
const ptsNeeded = nextBand.min - score;
if (ptsNeeded <= 0 || ptsNeeded > 20) return null; // too far away to show
return { grade: nextBand.grade, pts: ptsNeeded };
}
// ─── Grade diff bar ──────────────────────────────────────────────────────
function GradeDiff({ prev, curr, system }) {
const t = useT();
if (!prev || !curr) return null;
const delta = curr.score - prev.score;
if (delta === 0 && prev.grade === curr.grade) return null;
const sign = delta > 0 ? "+" : "";
const up = delta > 0;
const same = delta === 0;
return (
);
}
// ─── Landing Hero ────────────────────────────────────────────────────────
function LandingHero({ tweaks, onShowAuth }) {
const t = useT();
const scrollToGrader = () => {
// Clear the hash first so the browser always re-fires the anchor scroll,
// even if #grader is already in the URL (setting the same hash twice is a no-op).
history.replaceState(null, "", window.location.pathname + window.location.search);
window.location.hash = "#grader";
};
return (
{t("hero_eyebrow")}
{t("hero_h1a")} {t("hero_h1b")}
{t("hero_desc")}
{/* Sign-up is the better option — show it first and most prominently */}
);
}
// ─── Upsell banner (shown after first free grade for non-users) ──────────
function UpsellBanner({ onSignUp }) {
const t = useT();
return (
{t("upsell_title")}
{t("upsell_sub")}
✓{t("upsell_li1")}
✓{t("upsell_li2")}
);
}
// ─── Grade view ───────────────────────────────────────────────────────────
// ─── Score ring (new grade reveal) ───────────────────────────────────────
// Circular progress ring with the letter grade in the centre. Color-coded by
// score band, animates its fill on mount. Toggle via the "Result style" tweak.
// Shared tier palette for all score rings — bright → mid → deep → deepest,
// keyed by 0-100 score band. Used by both the grade-reveal ring and the
// dashboard/column/compare rings so every circle reads identically.
const auroraTier = (pct) => pct >= 80
? { bright: "#4ade80", mid: "#22c55e", deep: "#15803d", deepest: "#166534" }
: pct >= 60
? { bright: "#fde047", mid: "#eab308", deep: "#a16207", deepest: "#854d0e" }
: pct >= 45
? { bright: "#fdba74", mid: "#f97316", deep: "#c2410c", deepest: "#9a3412" }
: { bright: "#fca5a5", mid: "#ef4444", deep: "#b91c1c", deepest: "#991b1b" };
// Carries the loading ring's last fill value to the result ring (separate
// instances) so the reveal glides continuously to the exact grade — no snap.
let _scoreRingCarry = 0;
function ScoreRing({ score, grade, label, system, loading }) {
const size = 128, ringW = 15;
const pct = Math.max(0, Math.min(100, score || 0));
// Tier-aware vibrant fade: bright → mid → deep along the swept arc.
const tier = auroraTier(pct);
const loadTier = { bright: "#86efac", mid: "#34d399", deep: "#16a34a", deepest: "#15803d" };
const active = loading ? loadTier : tier;
const [draw, setDraw] = useState(loading ? 0 : (_scoreRingCarry || 0));
// Indeterminate "grading" fill — even linear creep toward a soft cap. Records
// its position so the result reveal can continue from exactly here.
useEffect(() => {
if (!loading) return;
let raf, start;
const RATE = 17; // points per second — steady, even motion
const CAP = 78; // sit just shy of "done" until the real score lands
const loop = (ts) => {
if (start == null) start = ts;
const v = Math.min(CAP, ((ts - start) / 1000) * RATE);
_scoreRingCarry = v;
setDraw(v);
raf = requestAnimationFrame(loop);
};
raf = requestAnimationFrame(loop);
return () => cancelAnimationFrame(raf);
}, [loading]);
// Result reveal — ONE clean eased glide from wherever loading left off onto
// the exact grade position, then stops dead (Rolex-sweep, no jump/snap).
// A safety timer guarantees the ring + number always land on the real grade
// even if rAF is paused (e.g. the tab was backgrounded mid-grade).
useEffect(() => {
if (loading) return;
let raf, start, done = false;
const from = _scoreRingCarry || 0;
const dur = 850;
const ease = (t) => 1 - Math.pow(1 - t, 3);
const finish = () => { if (!done) { done = true; setDraw(pct); _scoreRingCarry = 0; } };
const step = (ts) => {
if (start == null) start = ts;
const t = Math.min(1, (ts - start) / dur);
setDraw(from + (pct - from) * ease(t));
if (t < 1) raf = requestAnimationFrame(step);
else finish();
};
raf = requestAnimationFrame(step);
const safety = setTimeout(finish, dur + 500);
return () => { cancelAnimationFrame(raf); clearTimeout(safety); };
}, [loading, pct]);
// SVG ring with genuinely rounded stroke caps + a vibrant diagonal fade.
const R = (size - ringW) / 2;
const CIRC = 2 * Math.PI * R;
const dashOffset = CIRC * (1 - draw / 100);
return (
{loading ? "?" : grade}
{loading ? (
<>
{Math.round(draw)}/100
Grading…
{system.name}
>
) : (
<>
{Math.round(draw)}/100
{label}
{system.name}
>
)}
);
}
function GradeView({ system, setSystemId, setPrefs, user, freeLeft, weeklyLeft, planLimit, onGradeUsed, onSaveAttempt, onShowPricing, prefs, onShowAuth, onSaved, tweaks }) {
// Restore the last grade so it survives a refresh AND a trip to the
// Settings/Dashboard tabs (this panel unmounts on tab switch and would
// otherwise lose the result).
const lastGrade = (() => {
try {
const d = JSON.parse(localStorage.getItem("mr:lastGrade") || "null");
// Discard if it belongs to a different user account
if (d && d.userId && user?.id && d.userId !== user.id) {
localStorage.removeItem("mr:lastGrade");
return null;
}
return d;
} catch { return null; }
})();
const [text, setText] = useState(() => lastGrade?.text || "");
const [subject, setSubject] = useState(() => {
if (lastGrade?.subject) return lastGrade.subject;
const saved = prefs?.defaultSubject;
if (saved) return saved;
const cc = prefs?.country;
const lang = prefs?.feedbackLanguage || "English";
const list = cc ? (subjectsForLevel(cc, prefs?.gradeLevel, lang) || SUBJECTS) : SUBJECTS;
return list[0];
});
const [strictness, setStrictness] = useState(lastGrade?.strictness || prefs?.defaultStrictness || "Fair");
const [rubric, setRubric] = useState(lastGrade?.rubric || "");
const [loading, setLoading] = useState(false);
const [result, setResult] = useState(() => lastGrade?.result || null);
const [prevResult, setPrevResult] = useState(() => lastGrade?.prevResult || null); // for re-grade diff
const [error, setError] = useState(null);
const t = useT();
// Demo injection — call window.__demoGrade() from console to preview result UI
useEffect(() => {
window.__demoGrade = () => {
setText("The Great Gatsby is a novel written by F. Scott Fitzgerald in 1925. It explores themes of wealth, class, love, and the American Dream through the eyes of narrator Nick Carraway. The story centres on Jay Gatsby, a mysteriously wealthy man who throws lavish parties in the hope of reuniting with his lost love, Daisy Buchanan. Fitzgerald uses the green light at the end of Daisy's dock as a symbol of Gatsby's unreachable dream and, more broadly, the illusory nature of the American Dream itself. The novel is widely regarded as one of the greatest works of American literature, praised for its lyrical prose and its critique of the moral emptiness lurking beneath the glittering surface of the Jazz Age.");
setResult({
score: 81,
label: "Strong",
summary: "A confident and well-structured response that demonstrates solid understanding of the novel's core themes. The analysis of the green light symbol is handled with genuine insight, and the prose is clear and controlled throughout.",
strengths: [
"Strong grasp of the green light as a multi-layered symbol",
"Contextualises the novel effectively within the Jazz Age",
"Clear, fluent prose with good sentence variety",
"Accurate use of literary terminology (symbol, narrator, theme)"
],
weaknesses: [
"Argument could be sharpened — some claims stay at summary level",
"No direct quotation from the text to anchor the analysis",
"Conclusion restates the introduction rather than developing it"
],
feedback: "Push past paraphrase into argument: choose one moment in the text and interrogate it closely. A single well-chosen quotation, discussed in depth, will lift this from B to A territory.",
topTips: [],
annotations: []
});
};
return () => { delete window.__demoGrade; };
}, []);
// Demo: preview the grading (loading) phase. __demoGrading(ms) holds the
// loading state for ms (default 4000), then resolves into the demo result.
useEffect(() => {
window.__demoGrading = (ms = 4000) => {
setText("The Great Gatsby explores the American Dream through Jay Gatsby and the green light symbol.");
setResult(null);
setLoading(true);
setTimeout(() => { setLoading(false); window.__demoGrade(); }, ms);
};
return () => { delete window.__demoGrading; };
}, []);
const [saved, setSaved] = useState(() => lastGrade?.saved || false);
const [saveName, setSaveName] = useState(() => lastGrade?.saveName || "");
const [confirmDeleteDraft, setConfirmDeleteDraft] = useState(false);
const clearDraft = () => {
setResult(null);
setText("");
setSaveName("");
setSaved(false);
setPrevResult(null);
setConfirmDeleteDraft(false);
localStorage.removeItem("mr:lastGrade");
};
// Persist the current grade whenever it changes, so it survives refresh and
// tab switches. Cleared automatically when there's no result on screen.
useEffect(() => {
try {
if (result) {
localStorage.setItem("mr:lastGrade", JSON.stringify({
result, prevResult, text, subject, strictness, rubric, saveName, saved, ts: Date.now(),
userId: user?.id || null,
}));
} else {
localStorage.removeItem("mr:lastGrade");
}
} catch {}
}, [result, prevResult, text, subject, strictness, rubric, saveName, saved]);
// ─── file upload state ─────────────────────────────────────────
const [upload, setUpload] = useState(null); // { name, status: 'parsing'|'ok'|'err', error? }
const [dragOver, setDragOver] = useState(false);
const [textCapped, setTextCapped] = useState(false);
const fileInputRef = useRef(null);
// Setter that silently caps the text at MAX_TEXT_CHARS. Surfaces a small
// notice only when content is actually trimmed.
const setTextCapped_ = (raw) => {
if (raw.length > MAX_TEXT_CHARS) {
setText(raw.slice(0, MAX_TEXT_CHARS));
setTextCapped(true);
} else {
setText(raw);
if (textCapped) setTextCapped(false);
}
};
const handleFile = async (file) => {
if (!file) return;
setUpload({ name: file.name, status: "parsing" });
try {
const extracted = await extractTextFromFile(file);
const trimmed = (extracted || "").trim();
if (!trimmed) throw new Error("That file looks empty.");
const capped = trimmed.length > MAX_TEXT_CHARS;
const finalText = capped ? trimmed.slice(0, MAX_TEXT_CHARS) : trimmed;
setText(finalText);
setTextCapped(capped);
setUpload({
name: file.name,
status: "ok",
words: finalText.split(/\s+/).filter(Boolean).length,
capped,
});
} catch (e) {
setUpload({ name: file.name, status: "err", error: e.message || "Couldn't read that file." });
}
};
const onDrop = (e) => {
e.preventDefault();
setDragOver(false);
const f = e.dataTransfer?.files?.[0];
if (f) handleFile(f);
};
const wordCount = useMemo(() => text.trim() ? text.trim().split(/\s+/).length : 0, [text]);
const ready = text.trim().length >= 20 && !loading;
const blocked = !user && freeLeft <= 0;
const grade = async () => {
if (blocked) {
onGradeUsed("blocked");
return;
}
// Limit check: free users (signed-out OR plan=free) gated by SERVER (per IP/email).
// Paid users (student/pro) also gated server-side — no local cap.
const isFreeTier = !user || user.plan === "free";
if (isFreeTier) {
try {
const qs = user
? "?" + new URLSearchParams({ email: user.email, plan: user.plan || "free" }).toString()
: "?" + new URLSearchParams({ anonId: getAnonId() }).toString();
const r = await fetch("/api/usage" + qs);
const u = await r.json();
if (u.remaining <= 0) {
setError(`Weekly limit reached (${u.limit} grade${u.limit !== 1 ? "s" : ""}/week). ${user ? "Upgrade for more." : "Sign up for 2 free per week."}`);
return;
}
} catch {}
}
setLoading(true);setError(null);setResult(null);setSaved(false);
try {
const work = text.trim();
const words = work.split(/\s+/).filter(Boolean);
const wc = words.length;
// ── Call our Vercel serverless function (key stays server-side) ──
const r = await fetch("/api/grade", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
text: work,
email: user?.email || "",
anonId: user ? "" : getAnonId(),
plan: user?.plan || "free",
subject,
strictness,
rubric: rubric.trim(),
gradeLevel: prefs?.gradeLevel || "",
country: prefs?.country || "",
feedbackLanguage: prefs?.feedbackLanguage || "English",
system: { name: system.name, instruction: system.instruction, bands: system.bands }
})
});
if (!r.ok) {
const err = await r.json().catch(() => ({}));
throw new Error(err.error || "Backend error " + r.status);
}
const data = await r.json();
const newResult = parseGradeResult(data);
// Save old result for diff display when re-grading.
setResult((old) => { if (old) setPrevResult(old); return null; });
await new Promise(r => setTimeout(r, 0));
setResult(newResult);
// On narrow (stacked) screens scroll the result into view.
setTimeout(() => {
const el = document.getElementById("grade-result-panel");
if (el && window.innerWidth <= 880) {
const scrollEl = document.scrollingElement || document.documentElement;
scrollEl.scrollTop = scrollEl.scrollTop + el.getBoundingClientRect().top - 16;
}
}, 60);
// Server already incremented usage for all users via /api/grade.
// For anon users, also POST to /api/usage to keep the KV counter in sync.
if (!user) {
fetch("/api/usage", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ anonId: getAnonId() }),
}).catch(() => {});
}
// onGradeUsed triggers server usage refresh for logged-in users.
const preview = words.slice(0, 5).join(" ");
setSaveName(`${subject} — ${preview}${wc > 5 ? "…" : ""}`);
onGradeUsed("success");
} catch (err) {
console.error(err);
setError("Couldn't grade — " + (err.message || "try again."));
} finally {
setLoading(false);
}
};
const save = () => {
if (!result) return;
if (!user) {
onSaveAttempt(result, saveName, subject, system.id);
return;
}
const entries = loadEntries();
// Free-tier save cap: nudge to upgrade instead of saving silently dropped.
if (entries.length >= saveLimitFor(user.plan)) {
setError(`Free plan saves ${FREE_SAVE_LIMIT} grades. Upgrade for unlimited history.`);
onShowPricing?.();
return;
}
const entry = {
id: genId(),
name: saveName.trim() || `${subject} — untitled`,
subject,
systemId: system.id,
grade: result.grade,
label: result.label,
score: result.score,
// Persist the original submitted text so the dashboard can show it back.
// Capped to keep entry records reasonably small.
text: (text || "").slice(0, 8000),
// Persist the rubric (if any) — useful context when you revisit the entry.
rubric: (rubric || "").slice(0, 2000),
// Persist feedback so the dashboard can show context per entry.
summary: result.summary || "",
feedback: result.feedback || "",
strengths: Array.isArray(result.strengths) ? result.strengths : [],
weaknesses: Array.isArray(result.weaknesses) ? result.weaknesses : [],
annotations: Array.isArray(result.annotations) ? result.annotations : [],
// actualScore/actualGrade: user-entered override after they get the real grade back.
actualScore: null,
actualGrade: null,
weight: 1,
created: new Date().toISOString()
};
entries.unshift(entry);
// Navigate to the dashboard FIRST so the view switches instantly —
// the heavy result panel never re-renders on its way out.
onSaved?.(entries, entry);
// Persist after the navigation is queued (non-blocking).
saveEntries(entries);
if (user?.email) {
pushEntry(user.email, { ...entry, result }).then((ok) => {
if (!ok) enqueuePending({ ...entry, result });
});
}
};
return (
{!user && (
{t("free_badge_txt")}
)}
{
setPrefs({ country: cc, mySubjects: [] });
const sid = COUNTRY_TO_SYSTEM[cc];
if (sid) setSystemId(sid);
// Use language-aware list so subject stays in the user's language
const lang = prefs?.feedbackLanguage || "English";
const list = subjectsForLevel(cc, prefs?.gradeLevel, lang) || SUBJECTS;
setSubject(list[0]);
}} />
{user && user.plan === "free" &&
}
);
}
// Pick a representative score for a grade label inside a system. Uses the
// midpoint of the grade band so the projection averages stay sensible when
// the user enters a grade rather than a raw percent.
const gradeToScore = (gradeLabel, sysId) => {
const sys = SYSTEMS[sysId];
if (!sys) return null;
for (let i = 0; i < sys.bands.length; i++) {
if (sys.bands[i].grade === gradeLabel) {
const min = sys.bands[i].min;
const max = i === 0 ? 100 : Math.max(min, sys.bands[i - 1].min - 1);
return Math.round((min + max) / 2);
}
}
return null;
};
// Tint a grade band by how strong it is on the 0–100 scale. Works across every
// grading system because every band carries a 0–100 `min` threshold.
const bandTier = (min) => {
if (min >= 80) return "a"; // green
if (min >= 65) return "b"; // lime
if (min >= 50) return "c"; // amber
return "d"; // red
};
function EntryEditor({ entry, system, onSave, onClose, onDelete }) {
const [name, setName] = useState(entry.name);
const [subject, setSubject] = useState(entry.subject);
const [weight, setWeight] = useState(entry.weight);
const [actualGrade, setActualGrade] = useState(entry.actualGrade || null);
const [actualScore, setActualScore] = useState(entry.actualScore != null ? entry.actualScore : "");
// The editor follows the CURRENT dashboard system, not the entry's original
// one. Chips, derived labels, and the AI-grade hint are all translated via
// the underlying 0-100 score. Picking a chip locks actualScore to its band
// midpoint in this system.
const sys = system;
const aiScore = Math.round(entry.score || 0);
const aiGradeInCurrent = scoreToGrade(aiScore, sys.id);
// If a chip is selected, derive score from it. Otherwise use the typed score.
const resolved = useMemo(() => {
if (actualGrade) return { grade: actualGrade, score: gradeToScore(actualGrade, sys.id) };
if (actualScore === "" || actualScore == null) return { grade: null, score: null };
const n = Math.max(0, Math.min(100, Number(actualScore)));
return { grade: scoreToGrade(n, sys.id), score: n };
}, [actualGrade, actualScore, sys.id]);
// If the entry already has an actualScore, show what that grade is in the
// current system (might differ from stored actualGrade if system changed).
const storedActualGradeInCurrent = entry.actualScore != null
? scoreToGrade(entry.actualScore, sys.id) : null;
const pickChip = (g) => {
setActualGrade(g === actualGrade ? null : g);
setActualScore(""); // chip is now the source of truth
};
const typeScore = (v) => {
setActualScore(v);
setActualGrade(null); // typing overrides the chip
};
const save = () => {
onSave({
name: name.trim() || entry.name,
subject,
weight: Math.max(0, Number(weight) || 0),
actualScore: resolved.score,
actualGrade: resolved.grade,
});
onClose();
};
const dateStr = new Date(entry.created).toLocaleDateString(undefined, { month: "long", day: "numeric", year: "numeric" });
const wordCount = entry.text ? entry.text.split(/\s+/).filter(Boolean).length : 0;
const hasValue = resolved.grade != null;
return (
e.stopPropagation()}>
setName(e.target.value)}
spellCheck={false} />
{dateStr}·{sys.flag} {sys.name}
{/* AI result hero — the focal point of the card. */}
);
}
function SmartInsight({ entries, user, demoText, onShowPricing, onShowAuth }) {
const t = useT();
const isPaid = user?.plan === "student" || user?.plan === "pro";
// Free / signed-out users see a blurred teaser with seeded text + an upsell.
// Paid users see the live AI-generated insight.
const [text, setText] = useState(() =>
isPaid ? (localStorage.getItem("mr:insight") || "") : ""
);
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
const generate = async () => {
if (!isPaid) {
// Gated: always show pricing so the user can see plans and upgrade.
// If they're not logged in, the pricing modal handles prompting sign-in.
onShowPricing?.();
return;
}
if (entries.length === 0) return;
setLoading(true); setError(null);
try {
// Privacy-safe: ONLY send subject + score + weight + grade label.
// No original text, no AI feedback body, no email — pure aggregate signal.
const compact = entries.map((e) => ({
subject: e.subject,
score: Math.round(effScore(e)), // actual score if user confirmed, else AI estimate
aiPredicted: e.actualScore != null ? Math.round(e.score || 0) : null, // original AI score when overridden
confirmed: e.actualScore != null, // true = user entered their real grade
weight: e.weight,
label: e.label || "",
}));
// Try the server-side Groq endpoint first (works on Vercel).
// Fall back to window.claude.complete if we're in a preview env that
// doesn't have /api/insight wired up.
let out = "";
try {
const r = await fetch("/api/insight", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ entries: compact }),
});
if (r.ok) {
const data = await r.json();
out = data.text || "";
} else if (window.claude?.complete) {
out = await window.claude.complete(
`You are a warm study coach. Write 2-3 short sentences (max 60 words) about this student's grades — which subjects are strongest, which need work, and one concrete suggestion. Plain prose only.\n\n${JSON.stringify(compact)}`
);
} else {
const err = await r.json().catch(() => ({}));
throw new Error(err.error || "insight failed");
}
} catch (innerErr) {
if (window.claude?.complete) {
out = await window.claude.complete(
`You are a warm study coach. Write 2-3 short sentences (max 60 words) about this student's grades — which subjects are strongest, which need work, and one concrete suggestion. Plain prose only.\n\n${JSON.stringify(compact)}`
);
} else {
throw innerErr;
}
}
const trimmed = (out || "").trim();
setText(trimmed);
localStorage.setItem("mr:insight", trimmed);
} catch (e) {
setError("Couldn't generate insight right now");
} finally {
setLoading(false);
}
};
const upsellCopy = !user
? "View plans"
: "Upgrade to unlock";
const upsell = () => {
onShowPricing?.();
};
return (
Smart insight
{!isPaid && Pro}
{isPaid
? "Personalized analysis · only subject + score is sent, never your work"
: "AI-powered study coach · Student & Pro plans only"}
{!isPaid ? (
// Placeholder skeleton — intentionally not readable copy, so we
// don't put fake "insights" in front of users.
) : error ? {error}
: text ?
{text}
:
{t("insight_empty")}
}
{!isPaid && (
)}
);
}
// ─── Free-tier save banner ───────────────────────────────────────────
// Tells free users how many of their FREE_SAVE_LIMIT saved slots they've
// used, with a soft upgrade nudge. Only renders on the dashboard for
// signed-in free accounts that already have at least one entry.
function FreeSaveBanner({ count, onShowPricing }) {
const remaining = Math.max(0, FREE_SAVE_LIMIT - count);
const atCap = remaining === 0;
return (
{atCap ? (
<>
You've filled your {FREE_SAVE_LIMIT} free save slots. New grades won't save until you delete an old one — or upgrade for unlimited history.
>
) : (
<>
{count} of {FREE_SAVE_LIMIT} free saves used. {remaining} {remaining === 1 ? "slot" : "slots"} left on Free.
>
)}
);
}
function DashLocked({ user, onShowPricing }) {
const t = useT();
return (
{t("dash_lock_t")}
{t("dash_lock_d_text")}
{user ? "" : " Free accounts can grade once a week without an account."}
{t("dash_lock_li1")}
{t("dash_lock_li2")}
{t("dash_lock_li3")}
{t("dash_lock_li4")}
{t("dash_lock_li5")}
);
}
function Dashboard({ entries, setEntries, system, user, onShowPricing, demo, onShowAuth }) {
const isDemo = !!demo;
const t = useT();
// Demo mode keeps mutations in local state only — never persists.
const [demoEntries, setDemoEntries] = useState(() => SAMPLE_ENTRIES);
const liveEntries = isDemo ? demoEntries : entries;
const liveSetEntries = isDemo ? setDemoEntries : setEntries;
const [draggingId, setDraggingId] = useState(null);
const [overSubject, setOverSubject] = useState(null);
const [editingId, setEditingId] = useState(null);
const syncEntries = (next) => {
liveSetEntries(next);
if (isDemo) return;
saveEntries(next);
if (user?.email) {
// PUT replaces the whole list; retry once on transient failure so an
// edit or delete reliably reaches KV instead of silently reverting.
const put = (attempt = 0) =>
fetch("/api/save", {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ email: user.email, entries: next }),
})
.then((r) => { if (!r.ok && attempt < 1) setTimeout(() => put(attempt + 1), 600); })
.catch(() => { if (attempt < 1) setTimeout(() => put(attempt + 1), 600); });
put();
}
};
const updateEntry = (id, patch) => syncEntries(liveEntries.map((e) => e.id === id ? { ...e, ...patch } : e));
const removeEntry = (id) => syncEntries(liveEntries.filter((e) => e.id !== id));
// Group entries by their ACTUAL subject (localized, custom, or canonical) so
// the dashboard always follows whatever the user graded — not a fixed list.
const grouped = useMemo(() => {
const m = {};
for (const e of liveEntries) {
const s = (e.subject && String(e.subject).trim()) || "General";
(m[s] = m[s] || []).push(e);
}
// Order: known canonical subjects first (stable), then everything else
// alphabetically so custom/localized subjects have a predictable order.
const known = SUBJECTS.filter((s) => m[s]);
const rest = Object.keys(m).filter((s) => !SUBJECTS.includes(s)).sort((a, b) => a.localeCompare(b));
const ordered = {};
for (const s of [...known, ...rest]) ordered[s] = m[s];
return ordered;
}, [liveEntries]);
// Overall weighted average.
const overall = useMemo(() => {
if (!liveEntries.length) return null;
let t = 0, w = 0;
for (const e of liveEntries) {
const ew = Math.max(0, Number(e.weight) || 0);
t += effScore(e) * ew; w += ew;
}
return w === 0 ? 0 : t / w;
}, [liveEntries]);
const overallGrade = overall != null ? scoreToGrade(overall, system.id) : "—";
const totalWeight = liveEntries.reduce((a, e) => a + (Number(e.weight) || 0), 0);
// Drag handlers ────────────────────────────────────────────────────────
const onDragStart = (ev, id) => {
setDraggingId(id);
try { ev.dataTransfer.effectAllowed = "move"; ev.dataTransfer.setData("text/plain", id); } catch {}
};
const onDragEnd = () => { setDraggingId(null); setOverSubject(null); };
const onDragOver = (subject) => (ev) => { ev.preventDefault(); setOverSubject(subject); };
const onDragLeave = (subject) => () => { setOverSubject((s) => s === subject ? null : s); };
const onDrop = (subject) => (ev) => {
ev.preventDefault();
const id = draggingId || ev.dataTransfer?.getData("text/plain");
if (!id) return;
const entry = liveEntries.find((e) => e.id === id);
if (entry && entry.subject !== subject) updateEntry(id, { subject });
setDraggingId(null); setOverSubject(null);
};
const editing = editingId ? liveEntries.find((e) => e.id === editingId) : null;
const subjectsShown = Object.keys(grouped);
// Show only subjects that actually have entries — no stale empty columns.
// Every populated column is still a drop target for re-organizing.
const allSubjects = subjectsShown;
const firstName = user?.name ? user.name.split(" ")[0] : "";
return (
{isDemo && (
Demo
This is what your dashboard looks like once you save a few grades.
Try it — drag entries, click to edit. Nothing here is saved.
This lets the AI grade at the right level — a Year 9 essay and a university
paper need very different feedback.
Country
{/* Quick-pick top countries */}
{["Denmark","Germany","France","Sweden","Norway","Netherlands","United Kingdom","United States","Finland","Spain","Italy","Poland","Switzerland","Australia","Canada"].map((cc) => (
))}
{/* Full list via CountryPicker for others */}
setCountry(cc)} />
We never share this — it only shapes your feedback.
>}
{step === 2 && <>
What level are you at?
Helps calibrate subject suggestions and how your work is assessed.
);
}
// ─── OAuth popup callback detection ─────────────────────────────────────────
// When Google redirects back to us inside the popup, window.opener is set.
// We render a minimal screen instead of the full app so the popup is tiny
// and fast. Once the PKCE code is exchanged for a session it posts a message
// to the parent window and closes itself.
const IS_OAUTH_POPUP = !!(window.opener && window.opener !== window);
function OAuthPopupScreen() {
useEffect(() => {
if (!supabaseClient) { window.close(); return; }
const finish = (session) => {
if (!session) { window.close(); return; }
try {
window.opener?.postMessage(
{ type: "GRADECHECK_OAUTH_DONE", session },
window.location.origin
);
} catch (_) {}
// Brief pause so the BroadcastChannel propagates before the window closes.
setTimeout(() => window.close(), 300);
};
// The PKCE code exchange may already be done by the time this runs.
supabaseClient.auth.getSession().then(({ data }) => {
if (data.session) { finish(data.session); return; }
const { data: { subscription } } = supabaseClient.auth.onAuthStateChange(
(event, session) => {
if ((event === "SIGNED_IN" || event === "INITIAL_SESSION") && session) {
subscription.unsubscribe();
finish(session);
}
}
);
});
}, []);
return (
Signing you in…
);
}
// Root decides whether we are the OAuth popup callback or the real app.
function Root() {
if (IS_OAUTH_POPUP) return ;
return ;
}
ReactDOM.createRoot(document.getElementById("root")).render();